/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
	"context"
	"encoding/json"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/opa"
	"gitverse.ru/synapse/kubelatte/pkg/operator/controllers/lease"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"strings"
	"sync"
)

// TemplateReconciler reconciles a Template object
type TemplateReconciler struct {
	client.Client
	Scheme *runtime.Scheme
	once   sync.Once

	templateObj        v1alpha1.Template
	deletedTemplateObj v1alpha1.Template

	initedFlag              bool
	counterInstancesAtStart int
}

const (
	errorTemplatePrefix           = "Template error: "
	infoTemplatesOperationPostfix = " operation for a custom \"kind: Template\" configuration"
	RegoRulePrecompileFailed      = "RegoRulePrecompileFailed"
)

//+kubebuilder:rbac:groups=cache.kubelatte.io,resources=synapsetemplates,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cache.kubelatte.io,resources=synapsetemplates/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cache.kubelatte.io,resources=synapsetemplates/finalizers,verbs=update

func (r *TemplateReconciler) deferLog(req ctrl.Request, configData []byte, err error) {
	var status v1alpha1.LogStatus
	var inf string
	var level zapcore.Level

	if err == nil {
		inf = string(eventOperation) + infoTemplatesOperationPostfix
		status = v1alpha1.SuccessStatus
		level = 1
	} else {
		inf = errorTemplatePrefix + err.Error()
		status = v1alpha1.FailedStatus
		level = zap.ErrorLevel
	}
	logs.PersistLog(level, "rqUID", getRqUID(clusterName, req.Namespace, templateType, req.Name), "message", string(configData),
		"information", inf, "status", status, "operationName", eventOperation, "serviceReceiver", serviceReceiver)
}

func (r *TemplateReconciler) AddOrUpdateEventHndl(ctx context.Context, req ctrl.Request) (err error) {

	r.deferLog(req, []byte(r.templateObj.Spec.Data), err)

	return nil
}

func (r *TemplateReconciler) DeleteEventHndl(req ctrl.Request) (err error) {
	r.deferLog(req, []byte(r.deletedTemplateObj.Spec.Data), err)
	return nil
}

func (r *TemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	Mux.Lock()
	defer Mux.Unlock()

	r.once.Do(func() {
		r.initedFlag = true
	})

	// Check delete event
	err := r.Get(ctx, req.NamespacedName, &r.templateObj)
	if err != nil {
		if errors.IsNotFound(err) {
			if err = r.DeleteEventHndl(req); err != nil {
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, err
	}

	// Add or update event
	err = r.AddOrUpdateEventHndl(ctx, req)
	if err != nil {
		return ctrl.Result{}, err
	}

	return r.updateStatus(ctx, r.templateObj, false)
}

func (r *TemplateReconciler) updateStatus(ctx context.Context, template v1alpha1.Template, isAudit bool) (ctrl.Result, error) {
	if lease.AmILeader() {
		data, _ := json.Marshal(template.Spec)
		if CheckHashInStatus(template.Status.Conditions) {
			for i, condition := range template.Status.Conditions {
				if condition.Type == v1alpha1.LastHandledHash {
					sha1Hash := GetHash(data)
					if isAudit && strings.Compare(template.Status.Conditions[i].Message, sha1Hash) != 0 {
						logs.Debugf("Template status checker: version compare failed: %s != %s", template.Status.Conditions[i].Message, sha1Hash)
					}
					template.Status.Conditions[i].Message = sha1Hash
					template.Status.Conditions[i].LastTransitionTime = v1.Now()
				}
			}
		} else {
			template.Status.Conditions = append(template.Status.Conditions, AddHashCondition(data))
		}
		if template.Spec.Type == "rego" {
			template = updateRegoStatus(template)
		}
		err := r.Status().Update(ctx, &template, &client.SubResourceUpdateOptions{})
		if err != nil {
			logs.Error("Template controller: failed update status crd: " + err.Error())
			return ctrl.Result{}, err
		}
	}
	return ctrl.Result{}, nil
}

func (r *TemplateReconciler) manageEvents() predicate.Predicate {
	return predicate.Funcs{
		CreateFunc: func(e event.CreateEvent) bool {
			eventOperation = addEvent

			if e.Object.GetName() == LeaseLockName {
				return false
			}
			pmTemplatesTotal.Inc()
			obj := *e.Object.(*v1alpha1.Template)
			if IsOperatorInClusterMode() && !CheckNamespace(obj.Namespace, NSSelectorCommon) {
				return false
			}
			SendEvent("update", obj)
			return true
		},
		UpdateFunc: func(e event.UpdateEvent) bool {
			eventOperation = updateEvent

			if e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() && e.ObjectNew.GetName() != LeaseLockName {
				obj := *e.ObjectNew.(*v1alpha1.Template)
				if IsOperatorInClusterMode() && !CheckNamespace(obj.Namespace, NSSelectorCommon) {
					return false
				}
				SendEvent("update", obj)
				return true
			}

			return false
		},
		DeleteFunc: func(e event.DeleteEvent) bool {
			eventOperation = deleteEvent

			r.deletedTemplateObj = *e.Object.(*v1alpha1.Template)
			if e.Object.GetName() == LeaseLockName {
				return false
			}
			if IsOperatorInClusterMode() && !CheckNamespace(r.deletedTemplateObj.Namespace, NSSelectorCommon) {
				return false
			}
			pmTemplatesTotal.Dec()
			SendEvent("delete", r.deletedTemplateObj)
			return true
		},
	}
}

// SetupWithManager sets up the controller with the Manager.
func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1alpha1.Template{}).
		WithEventFilter(r.manageEvents()).
		Complete(r)
}

func SendEvent(tp string, obj v1alpha1.Template) {
	var eventName = ""
	switch tp {
	case "delete":
		if obj.Spec.Type == "rego" {
			eventName = opa.RegoDeleteTemplate
		} else {
			eventName = StorageDeleteTemplateEvent
		}
		break
	case "update":
		if obj.Spec.Type == "rego" {
			eventName = opa.RegoUpdateTemplate
		} else {
			eventName = StorageUpdateTemplateEvent
		}
		break
	}

	err := util.SendEventWithErr(eventName, obj)
	if err != nil {
		logs.Errorf("Send event error %s", err)
	}
}

func updateRegoStatus(template v1alpha1.Template) v1alpha1.Template {

	var _, err = opa.OpaController.Precompile(template.Spec.Data)
	if err != nil {
		template.Status.Phase = v1alpha1.Failed
		condition := v1alpha1.Condition{
			Type:               RegoRulePrecompileFailed,
			LastTransitionTime: v1.Now(),
			Status:             "True",
			Message:            err.Error(),
		}
		for i, _ := range template.Status.Conditions {
			if template.Status.Conditions[i].Type == RegoRulePrecompileFailed {
				template.Status.Conditions[i] = condition
				return template
			}
		}
		template.Status.Conditions = append(template.Status.Conditions, condition)
	} else {
		template.Status.Phase = v1alpha1.Running
		template.Status.Conditions = removeCondition(template.Status.Conditions, RegoRulePrecompileFailed)
	}
	return template
}

func (r *TemplateReconciler) HealthCheck() bool {
	return r.initedFlag
}
